第 1 章 引论
建议 1:理解 Pythonic 概念
Pythonic
当你输入 import this
就会显示 zen of python
美丽胜于丑陋。
显式优于隐式。
简单比复杂好。
复合胜于复杂。
平面比嵌套好。
稀疏比密集好。
可读性是重要的。
特殊情况不足以打破规则。
虽然实用性胜过纯粹。
除了显示错误,错误永远不应该沉默。
代码风格
充分体现python动态语言的特色,类似于
# 变量交换
a, b = b, a
# 上下文管理
with open(path, 'r') as f:
do_sth_with(f)
# 不应当过分地追求奇技淫巧
a = [1, 2, 3, 4]
a[::-1] # 不推荐。好吧,自从学了切片我一直用的这个
list(reversed(a)) # 推荐
然后表扬了 Flask 框架,提到了 generator 之类的特性尤为 Pythonic,有个包和模块的约束:
- 包和模块的命名采用小写、单数形式,而且短小
- 包通常仅作为命名空间,如只含空的__init__.py文件
建议 2:编写 Pythonic 代码
避免劣化代码
- 避免只用大小写区分不同的对象
- 避免使用容易引起混淆的名称
- 不要害怕过长的变量名
深入认识python有助于编写pythonic代码
- 全面掌握 python 提供的特性,包括语言和库
- 随着时间推移,要不断更新知识
- 深入学习业界公认的 pythoni 代码
- 编写符合 pep8 的代码规范(就是让你使用pycharm)
建议 3:理解 Python 与 C 语言的不同之处
- Python 使用代码缩进的方式来分割代码块,不要混用 Tab 键和空格
- Python 中单、双引号的效果相同(个人建议使用单引号,在面对其他语言的双引号源码时不必再转义)
- 三元操作符:x if bool else y(原因是作者认为应该用可读性更好的方式表达)
- 用其他方法替代 switch-case
建议 4:在代码中适当添加注释
- 块和行注释仅仅注释复杂的操作、算法等
- 注释和代码隔开一段距离
- 给外部可访问的函数和方法添加文档注释
- 推荐在文件头中包含 copyright 申明、模块描述等
另外,编写代码应该朝代码即文档的方向进行,但仍应该注重注释的使用
建议 5:通过适当添加空行使代码布局更为优雅、合理
- 表达完一个完整思路后,应该用空白行间隔,尽量不要在一段代码中说明几件事。
- 尽量保持上下文的易理解性,比如调用者在上,被调用者在下
- 避免过长的代码行,超过80个字符应该使用行连接换行(还是让你使用pycharm)
- 水平对齐毫无意义,不要用多余空格保持对齐
- 空格的使用要能够在需要使用时强调警示读者(符合PEP8规范)
建议 6:编写函数的 4 个原则
- 函数设计要尽量短小,嵌套层次不宜过深
- 函数申明应该做到合理、简单、易于使用
- 函数参数设计应该考虑向下兼容
- 一个函数只做一件事,尽量保证函数语句粒度的一致性
Python 中函数设计的好习惯还包括:不要在函数中定义可变对象作为默认值,使用异常替换返回错误,保证通过单元测试等。
# 关于函数设计的向下兼容
def readfile(filename): # 第一版本
pass
def readfile(filename, log): # 第二版本
pass
def readfile(filename, logger=logger.info): # 合理的设计
pass
最后还有个函数可读性良好的例子:
def GetContent(ServerAdr, PagePath):
http = httplib.HTTP(ServerAdr)
http.putrequest('GET', PagePath)
http.putheader('Accept', 'text/html')
http.putheader('Accept', 'text/plain')
http.endheaders()
httpcode, httpmsg, headers = http.getreply()
if httpcode != 200:
raise "Could not get document: Check URL and Path."
doc = http.getfile()
data = doc.read() # 此处是不是应该使用 with ?
doc.close
return data
def ExtractData(inputstring, start_line, end_line):
lstr = inputstring.splitlines() # split
j = 0
for i in lstr:
j += 1
if i.strip() == start_line: slice_start = j
elif i.strip() == end_line: slice_end = j
return lstr[slice_start:slice_end]
def SendEmail(sender, receiver, smtpserver, username, password, content):
subject = "Contented get from the web"
msg = MIMEText(content, 'plain', 'utf-8')
msg['Subject'] = Header(subject, 'utf-8')
smtp = smtplib.SMTP()
smtp.connect(smtpserver)
smtp.login(username, password)
smtp.sendmail(sender, receiver, msg.as_string())
smtp.quit()
建议 7:将常量集中到一个文件
在Python中应当如何使用常量:
- 常量名全部大写
- 将存放常量的文件命名为constant.py
示例为:
class _const:
class ConstError(TypeError): pass
class ConstCaseError(ConstError): pass
def __setattr__(self, name, value):
if self.__dict__.has_key(name):
raise self.ConstError, "Can't change const.%s" % name
if not name.isupper():
raise self.ConstCaseError, \
'const name "%s" is not all uppercase' % name
self.__dict__[name] = value
import sys
sys.modules[__name__] = _const()
import const
const.MY_CONSTANT = 1
const.MY_SECOND_CONSTANT = 2
const.MY_THIRD_CONSTANT = 'a'
const.MY_FORTH_CONSTANT = 'b'
其他模块中引用这些常量时,按照如下方式进行即可:
from constant import const
print(const.MY_CONSTANT)
第 2 章 编程惯用法
建议 8:利用 assert 语句来发现问题
断言的判断会对性能有所影响,因此要分清断言的使用场合:
- 断言应使用在正常逻辑无法到达的地方或总是为真的场合
- python本身异常处理能解决的问题不需要用断言
- 不要使用断言检查用户输入,而使用条件判断
- 在函数调用后,当需要确认返回值是否合理时使用断言
- 当条件是业务的先决条件时可以使用断言
代码示例:
>>> y = 2
>>> assert x == y, "not equals"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError: not equals
>>> x = 1
>>> y = 2
# 以上代码相当于
>>> if __debug__ and not x == y:
... raise AssertionError("not equals")
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
AssertionError: not equals
运行是加入-O参数可以禁用断言。
建议 9:数据交换的时候不推荐使用中间变量
>>> Timer('temp = x; x = y; y = temp;', 'x = 2; y = 3').timeit()
0.059251302998745814
>>> Timer('x, y = y, x', 'x = 2; y = 3').timeit()
0.05007316499904846
对于表达式x, y = y, x,在内存中执行的顺序如下:
1. 先计算右边的表达式y, x,因此先在内存中创建元组(y, x),其标识符和值分别为y, x及其对应的值,其中y和x是在初始化已经存在于内存中的对象。
2. 计算表达式左边的值并进行赋值,元组被依次分配给左边的标识符,通过解压缩,元组第一标识符y分配给左边第一个元素x,元组第二标识符x分配给左边第一个元素y,从而达到交换的目的。
(简单来说,直接交换符合pythonic且性能最佳,这么做就对了)
建议 10:充分利用 Lazy evaluation 的特性
(就是生成器)
Lazy evaluation常被译为延迟计算,体现在用 yield 替换 return 使函数成为生成器,好处主要有两方面:
- 避免不必要的计算,带来性能提升
- 节省空间,使无限循环的数据结构成为可能
def fib():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
建议 11:理解枚举替代实现的缺陷
使用 flufl.enum 实现枚举
建议 12:不推荐使用 type 来进行类型检查
使用 isinstance 来进行类型检查(注意上下包含关系就行)
建议 13:尽量转换为浮点类型后再做除法
py2.x:转换浮点类型后再做除法
建议 14:警惕 eval() 的安全漏洞
eval具有安全漏洞,建议使用安全性更好的ast.literal_eval。
建议 15:使用 enumerate() 获取序列迭代的索引和值
>>> li = ['a', 'b', 'c', 'd', 'e']
>>> for i, e in enumerate(li):
... print('index: ', i, 'element: ', e)
...
index: 0 element: a
index: 1 element: b
index: 2 element: c
index: 3 element: d
index: 4 element: e
# enumerate(squence, start=0) 内部实现
def enumerate(squence, start=0):
n = start
for elem in sequence:
yield n, elem # 666
n += 1
# 明白了原理我们自己也来实现一个反序的
def reversed_enumerate(squence):
n = -1
for elem in reversed(sequence):
yield len(sequence) + n, elem
n -= 1
(此方式相比从列表里放索引取值更加优雅)
建议 16:分清 == 与 is 的适用场景
比较有趣的:
>>> s1 = 'hello world'
>>> s2 = 'hello world'
>>> s1 == s2
True
>>> s1 is s2
False
>>> s1.__eq__(s2)
True
>>> a = 'Hi'
>>> b = 'Hi'
>>> a == b
True
>>> a is b
True
为了提高系统性能,对于较小的字符串会保留其值的一个副本,当创建新的字符串时直接指向该副本,所以a和b的 id 值是一样的,同样对于小整数[-5, 257)也是如此:
注意is不相当于 ==, is 是对 id 方法做的 == 。
建议 17:考虑兼容性,尽可能使用 Unicode
python2.x 这是无敌深坑,需要刻苦学习掌握(python3偶尔也会碰到这种问题,但避免了大多数这种可能)
建议 18:构建合理的包层次来管理 module
(__init__是对包的头文件定制) 本质上每一个 Python 文件都是一个模块,使用模块可以增强代码的可维护性和可重用性,在较大的项目中,我们需要合理地组织项目层次来管理模块,这就是包(Package)的作用。
一句话说包:一个包含__init__.py 文件的目录。包中的模块可以通过.进行访问,即包名.模块名。那么这init.py文件有什么用呢?最明显的作用就是它区分了包和普通目录,在该文件中申明模块级别的 import 语句从而变成了包级别可见,另外在该文件中定义__all__变量,可以控制需要导入的子包或模块。
这里给出一个较为合理的包组织方式,是FlaskWeb 开发:基于Python的Web应用开发实战一书中推荐而来的:
|-flasky
|-app/ # Flask 程序
|-templates/ # 存放模板
|-static/ # 静态文件资源
|-main/
|-__init__.py
|-errors.py # 蓝本中的错误处理程序
|-forms.py # 表单对象
|-views.py # 蓝本中定义的程序路由
|-__init__.py
|-email.py # 电子邮件支持
|-models.py # 数据库模型
|-migrations/ # 数据库迁移脚本
|-tests/ # 单元测试
|-__init__.py
|-test*.py
|-venv/ # 虚拟环境
|-requirements/
|-dev.txt # 开发过程中的依赖包
|-prod.txt # 生产过程中的依赖包
|-config.py # 储存程序配置
|-manage.py # 启动程序以及其他的程序任务
第 3 章:基础语法
建议 19:有节制地使用 from…import 语句
Python 提供三种方式来引入外部模块:import语句、from…import语句以及__import__函数,其中__import__函数显式地将模块的名称作为字符串传递并赋值给命名空间的变量。
使用import需要注意以下几点:
- 优先使用import a的形式
- 有节制地使用from a import A
- 尽量避免使用from a import *
为什么呢?我们来看看 Python 的 import 机制,Python 在初始化运行环境的时候会预先加载一批内建模块到内存中,同时将相关信息存放在sys.modules中,我们可以通过 sys.modules.items() 查看预加载的模块信息,当加载一个模块时,解释器实际上完成了如下动作:
- 在 sys.modules 中搜索该模块是否存在,如果存在就导入到当前局部命名空间,如果不存在就为其创建一个字典对象,插入到 sys.modules 中。
- 加载前确认是否需要对模块对应的文件进行编译,如果需要则先进行编译。
- 执行动态加载,在当前命名空间中执行编译后的字节码,并将其中所有的对象放入模块对应的字典中。
>>> dir()
['__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__']
>>> import test
testing module import
>>> dir()
['__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'test']
>>> import sys
>>> 'test' in sys.modules.keys()
True
>>> id(test)
140367239464744
>>> id(sys.modules['test'])
140367239464744
>>> dir(test)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'b']
>>> sys.modules['test'].__dict__.keys()
dict_keys(['__file__', '__builtins__', '__doc__', '__loader__', '__package__', '__spec__', '__name__', 'b', 'a', '__cached__'])
从上可以看出,对于用户自定义的模块,import 机制会创建一个新的 module。 将其加入当前的局部命名空间中,同时在 sys.modules 也加入该模块的信息,但本质上是在引用同一个对象,通过test.py所在的目录会多一个字节码文件。
(这节说的是,盲目使用from…import…会带来:
1. 命名空间冲突
2. 循环嵌套导入)
建议 20:优先使用 absolute import 来导入模块
(py3 中 relative import方法已被移除,不用操心)